use chrono::Datelike;
use sha2::{Digest, Sha256};
use std::{path::Path, str::FromStr, sync::Arc};

use crate::{
    errors::cons_error,
    mdb::{MClient, ModelEx, MDB},
    models::assets::AssetsModel,
    AppError, AppResult, Config, Pagination, PaginationQuery,
};
use actix_multipart::Multipart;
use futures::StreamExt;
use log::debug;
use path_slash::PathBufExt;
use rand::{prelude::SliceRandom, random};
use serde::{Deserialize, Serialize};
use wither::{
    bson::{doc, oid::ObjectId, Bson, Document},
    mongodb::options::FindOptions,
    Model,
};
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct AssetsService {
    mdb: MDB,
    m_client: MClient,
    config: Arc<Config>,
}
use hex::encode as hex_encode;
use tokio::fs::{create_dir_all, remove_file, write as write_file};
impl AssetsService {
    pub fn new(config: Arc<Config>, mdb: MDB, m_client: MClient) -> Self {
        Self {
            config,
            mdb,
            m_client,
        }
    }
    pub async fn upload_assets(
        &self,
        mut multipart: Multipart,
        owner_id: Option<ObjectId>,
    ) -> AppResult<Vec<AssetsModel>> {
        let now = chrono::Utc::now();
        let relative_path = Path::new(format!("{}", now.date().year()).as_str()).join(format!(
            "{}{}",
            now.date().month(),
            now.date().day()
        ));

        let base_path = self.config.upload.upload_base_path.clone();
        let base_path = Path::new(base_path.as_str())
            .join(relative_path.clone())
            .to_str()
            .unwrap_or("")
            .to_string();
        create_dir_all(base_path.clone()).await?;
        let mut res_vec = vec![];
        'parts: while let Some(Ok(mut field)) = multipart.next().await {
            let content_type = field.content_type().clone();
            let mut hasher = Sha256::new();
            let mut data_vec = vec![];
            while let Some(data) = field.next().await {
                let data = match data {
                    Ok(o) => o,
                    Err(_) => {
                        continue 'parts;
                    }
                };
                hasher.update(data.clone());
                data_vec.extend(data);
            }
            let hex_str = hex_encode(hasher.finalize());
            let disposition = field.content_disposition();
            let hex_filename = disposition
                .get_filename()
                .and_then(|filename| Path::new(filename).extension().map(|ext| ext.to_str()))
                .flatten()
                .map(|ext_str| format!("{}.{}", hex_str, ext_str))
                .unwrap_or(hex_str);

            let save_path = Path::new(base_path.as_str()).join(hex_filename.clone());
            let relative_path = relative_path.join(hex_filename);
            debug!("save file {:?}", save_path);
            write_file(save_path, data_vec).await?;
            let mut model = AssetsModel::new_with_site_path(
                relative_path.to_slash().unwrap_or_else(|| "".into()),
            );
            model.mime = Some(content_type.to_string());
            model.filename = disposition.get_filename().map(|f| f.to_string());
            model.filename_ext = disposition.get_filename_ext().map(|f| f.to_string());
            model.owner_id = owner_id;
            model.save(&*self.mdb.clone(), None).await?;
            res_vec.push(model)
        }
        Ok(res_vec)
    }

    pub async fn find_all_assets(
        &self,
        page_query: &PaginationQuery,
        req: FindAllAssetsReq,
    ) -> AppResult<Pagination<AssetsModel>> {
        AssetsModel::find_and_pagination(self.mdb.clone(), Some(req.into()), page_query).await
    }
    pub async fn search_all_assets<W: Into<Option<Document>>>(
        &self,
        page_query: &PaginationQuery,
        where_value: W,
    ) -> AppResult<Pagination<AssetsModel>> {
        let where_value: Option<Document> = where_value.into();
        AssetsModel::find_and_pagination(self.mdb.clone(), where_value, page_query).await
    }

    pub async fn delete_assets(&self, req: &DeleteAssetsReq) -> AppResult<()> {
        let oid = ObjectId::from_str(&req.assets_id)?;
        let assets_model = AssetsModel::find_one(&*self.mdb.clone(), Some(doc! {"_id":oid}), None)
            .await?
            .ok_or(AppError::ConsError(cons_error::MSG_MDB_NOT_FOUND))?;
        let other_count = AssetsModel::collection(&*self.mdb.clone())
            .count_documents(Some(doc! {"path":&assets_model.path}), None)
            .await?;
        if other_count < 2 {
            // 当所有相同路径资源都被删除时删除硬盘资源
            debug!("相同路径资源已全部清除，开始清除 {}", assets_model.path);
            remove_file(Path::new(&self.config.upload.upload_base_path).join(&assets_model.path))
                .await
                .ok();
            assets_model.delete(&*self.mdb.clone()).await?;
        } else if req.force {
            // 强制删除所有
            debug!("强制删除所有资源 {}", assets_model.path);
            remove_file(Path::new(&self.config.upload.upload_base_path).join(&assets_model.path))
                .await
                .ok();
            AssetsModel::collection(&*self.mdb.clone())
                .delete_many(doc! {"path":&assets_model.path}, None)
                .await?;
        } else {
            assets_model.delete(&*self.mdb.clone()).await?;
        }

        Ok(())
    }

    pub async fn draw_assets(&self, count: u64) -> AppResult<Vec<AssetsModel>> {
        let assets_count = AssetsModel::collection(&*self.mdb.clone())
            .count_documents(None, None)
            .await?;

        let draw_count = count.min(assets_count);
        let offset_range = assets_count - draw_count;
        let offset = (offset_range as f64 * random::<f64>()).floor() as u64;
        let options = FindOptions::builder().skip(offset).build();

        let mut cursor = AssetsModel::find(
            &*self.mdb.clone(),
            Some(doc! {"owner_id":Bson::Null}),
            Some(options),
        )
        .await?;

        let mut res = vec![];
        while let Some(item) = cursor.next().await {
            let item = item?;
            res.push(item)
        }
        res.shuffle(&mut rand::thread_rng());
        Ok(res)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FindAllAssetsReq {
    pub owner_id: Option<String>,
    #[serde(default = "Default::default")]
    pub is_foreign: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteAssetsReq {
    pub assets_id: String,
    #[serde(default = "Default::default", rename = "f")]
    pub force: bool,
}

impl From<FindAllAssetsReq> for Document {
    fn from(req: FindAllAssetsReq) -> Self {
        let mut res_doc = Document::new();
        if let Some(owner_id) = req
            .owner_id
            .as_deref()
            .map(ObjectId::from_str)
            .and_then(Result::ok)
        {
            res_doc.insert("owner_id", owner_id);
        }
        res_doc.insert("is_foreign", req.is_foreign);
        res_doc
    }
}
